﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using DynORM.Interfaces;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml;
using DynORM.TypeMappingSubsystem;

namespace DynORM.ValueObjects
{
    [XmlRoot(ElementName = ForeignKey.ForeignKeyTag)]
    public class ForeignKey : IForeignKey, IXmlSerializable
    {
        private RelationType relationType; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string tableName; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private Dictionary<int, IField> fields; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string relatedTableName; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private Dictionary<int, IField> relatedFields; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer
        private string name; //had to relax "readonly" restriction of this member in favor to init logic of xml serializer

        public ForeignKey(string tableName, IDictionary<int, IField> fields, string relatedTableName, IDictionary<int, IField> relatedFields, string name)
        {
            this.relationType = RelationType.ForeignKey;
            this.fields = new Dictionary<int, IField>();
            this.relatedTableName = String.Empty;
            this.relatedFields = new Dictionary<int, IField>();

            this.tableName = tableName ?? String.Empty;
            foreach (KeyValuePair<int, IField> f in fields)
                this.fields.Add(f.Key, f.Value);

            this.relatedTableName = relatedTableName ?? String.Empty;
            foreach (KeyValuePair<int, IField> rf in relatedFields)
                this.relatedFields.Add(rf.Key, rf.Value);

            this.name = name;
        }

        public ForeignKey(IForeignKeyDescriptor relationDescriptor, params object[] values)
        {
            this.relationType = RelationType.ForeignKey;
            this.fields = new Dictionary<int, IField>();
            this.relatedFields = new Dictionary<int, IField>();

            int i = 0;
            foreach (KeyValuePair<int, IFieldDescriptor> kv in relationDescriptor.FieldDescriptors.OrderBy<KeyValuePair<int, IFieldDescriptor>, int>(fd => fd.Key))
                this.fields.Add(kv.Key, new Field(kv.Value, values[i++]));
            this.tableName = relationDescriptor.TableName;

            foreach (KeyValuePair<int, IFieldDescriptor> kv in relationDescriptor.RelatedFieldDescriptors.OrderBy<KeyValuePair<int, IFieldDescriptor>, int>(fd => fd.Key))
                this.relatedFields.Add(kv.Key, new Field(kv.Value, values[i++]));
            this.relatedTableName = relationDescriptor.RelatedTableName;

            this.name = relationDescriptor.Name;
            //this.uniqueName = relationDescriptor.UniqueName;
        }

        //for serializer
        private ForeignKey()
        {
            this.relationType = RelationType.ForeignKey;
            this.fields = new Dictionary<int, IField>();
            this.relatedFields = new Dictionary<int, IField>();
        }

        public const string ForeignKeyTag = "ForeignKey";
        public const string ForeignKeyNameAttr = "Name";
        public const string ForeignKeyTableNameAttr = "TableName";
        public const string ForeignKeyFieldsTag = "ForeignKeyFields";
        public const string ForeignKeyRelatedTableNameAttr = "RelatedTableName";
        public const string ForeignKeyRelatedFieldsTag = "ForeignKeyRelatedFields";

        public string Name
        {
            get
            {
                return this.name;
            }
        }

        public RelationType RelationType
        {
            get { return relationType; }
        }

        public string TableName
        {
            get { return this.tableName; }
        }

        public IDictionary<int, IField> Fields
        {
            get { return this.fields; }
        }

        public string RelatedTableName
        {
            get { return this.relatedTableName; }
        }

        public IDictionary<int, IField> RelatedFields
        {
            get
            {
                return this.relatedFields;
            }
        }

        public CheckIntegrityResult CheckValuesIntegrity(bool usePredefinedConvention)
        {
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            //just because we construct Relations not only from RelationDescriptors we have to recheck structure integrity before value integrity
            CheckIntegrityResult checkStructureIntegrityRes = this.GetDescriptor().CheckStructureIntegrity(false);
            if (checkStructureIntegrityRes != passed)
                return checkStructureIntegrityRes;

            for (int i = 0; i < this.fields.Count; i++ ) //the fact that number of fields are equal we've checked during structure verifications 
            {
                IField f = this.fields[i];
                IField rf = this.relatedFields[i];

                if (f.FieldValue != null && f.CheckValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field composing the foreign key failed its structure integrity check. See inner CheckIntegrityResult.", f.CheckValuesIntegrity(false));

                if (rf.FieldValue != null && rf.CheckValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field composing the foreign key failed its structure integrity check. See inner CheckIntegrityResult.", rf.CheckValuesIntegrity(false));

                if (f.FieldValue != null && rf.FieldValue != null && !f.FieldValue.Equals(rf.FieldValue))
                    return new CheckIntegrityResult(false, "Field and related field contain not equal values.");
                if (usePredefinedConvention)
                {
                    if (!(f.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || f.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(f.FieldName, String.Format(@"{0}_{1}_FK_{2}.*", this.TableName, this.RelatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0}_{1}_{2}_FK (or {0}_{1}_{2}_FK_m_n in case of composite key and presence several FK that links this two tables).", ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs));

                    if (!(rf.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || rf.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(rf.FieldName, String.Format(@"{0}.*", PrimaryKeyDescriptor.PredefinedFieldNameForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0} (or {0}_n in case of composite key).", PrimaryKeyDescriptor.PredefinedFieldNameForIDs));
                }
            }

            //if (String.Compare(relatedTableName, tableName, StringComparison.OriginalIgnoreCase) == 0)
            //    return new CheckIntegrityResult(false, "For this type of relation given table and its related table shouldn't be the same.");

            return passed;
        }

        public CheckIntegrityResult CheckOnlyValuesIntegrity(bool usePredefinedConvention)
        {
            //this method is used in case when several rows from the same table are retrieved. in this case structure (RowDescriptor) is checked only once and values (Rows) check do not include structure tests
            CheckIntegrityResult passed = new CheckIntegrityResult(true, String.Empty);

            for (int i = 0; i < this.fields.Count; i++) //the fact that number of fields are equal we've checked during structure verifications 
            {
                IField f = this.fields[i];
                IField rf = this.relatedFields[i];

                if (f.FieldValue != null && f.CheckOnlyValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field composing the foreign key failed its structure integrity check. See inner CheckIntegrityResult.", f.CheckValuesIntegrity(false));

                if (rf.FieldValue != null && rf.CheckOnlyValuesIntegrity(false) != passed)
                    return new CheckIntegrityResult(false, "One field composing the foreign key failed its structure integrity check. See inner CheckIntegrityResult.", rf.CheckValuesIntegrity(false));

                if (f.FieldValue != null && rf.FieldValue != null && !f.FieldValue.Equals(rf.FieldValue))
                    return new CheckIntegrityResult(false, "Field and related field contain not equal values.");
                if (usePredefinedConvention)
                {
                    if (!(f.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || f.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(f.FieldName, String.Format(@"{0}_{1}_FK_{2}.*", this.TableName, this.RelatedTableName, ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0}_{1}_{2}_FK (or {0}_{1}_{2}_FK_m_n in case of composite key and presence several FK that links this two tables).", ForeignKeyDescriptor.PredefinedFieldNameForForeignIDs));

                    if (!(rf.FieldType == PrimaryKeyDescriptor.PredefinedTypeForIDs || rf.FieldType == Types.GetCLRNullableType(PrimaryKeyDescriptor.PredefinedTypeForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field type must be {0}.", PrimaryKeyDescriptor.PredefinedTypeForIDs.Name));
                    if (!Regex.IsMatch(rf.FieldName, String.Format(@"{0}.*", PrimaryKeyDescriptor.PredefinedFieldNameForIDs)))
                        return new CheckIntegrityResult(false, String.Format("Field name must be {0} (or {0}_n in case of composite key).", PrimaryKeyDescriptor.PredefinedFieldNameForIDs));
                }
            }

            //if (String.Compare(relatedTableName, tableName, StringComparison.OriginalIgnoreCase) == 0)
            //    return new CheckIntegrityResult(false, "For this type of relation given table and its related table shouldn't be the same.");

            return passed;
        }

        public IORMObjectDescriptor GetDescriptor()
        {
            Dictionary<int, IFieldDescriptor> fieldsDescriptors = new Dictionary<int, IFieldDescriptor>();
            Dictionary<int, IFieldDescriptor> relatedFieldDescriptors = new Dictionary<int, IFieldDescriptor>();
            foreach (KeyValuePair<int, IField> f in this.fields)
                fieldsDescriptors.Add(f.Key, (IFieldDescriptor)f.Value.GetDescriptor());
            foreach (KeyValuePair<int, IField> rf in this.relatedFields)
                relatedFieldDescriptors.Add(rf.Key, (IFieldDescriptor)rf.Value.GetDescriptor());
            return new ForeignKeyDescriptor(this.tableName, fieldsDescriptors, this.relatedTableName, relatedFieldDescriptors, this.Name);
        }

        #region Value-based equality semantics implementation
        public override bool Equals(object obj)
        {
            //ForeignKey that = obj as ForeignKey;
            //if (Object.ReferenceEquals(that, null))
            if (!(obj is ForeignKey))
                return false;
            ForeignKey that = (ForeignKey)obj;

            if ((ForeignKeyDescriptor)this.GetDescriptor() != (ForeignKeyDescriptor)that.GetDescriptor())
                return false;

            for (int i = 0; i < this.Fields.Count; i++ ) //we can be sure in equality of field number because descriptors are equal
            {
                Field thisF = (Field)this.Fields[i];
                Field thatF = (Field)that.Fields[i];
                if (thisF != thatF) return false;
            }

            return true;
        }

        public override int GetHashCode()
        {
            return this.TableName.GetHashCode();
        }

        public static bool operator ==(ForeignKey r1, IRelation r2)
        {
            if (Object.ReferenceEquals(r1, null))
                return false;

            return r1.Equals(r2);
        }

        public static bool operator !=(ForeignKey r1, IRelation r2)
        {
            if (Object.ReferenceEquals(r1, null))
                return false;

            return !r1.Equals(r2);
        }

        #endregion

        #region IXmlSerializable and auxiliary CreateFromXml member
        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            bool related = false;
            int i = 0;

            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        //if (string.Compare(reader.Name, ForeignKeyUniqueNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                        //    this.uniqueName = reader.Value;
                                        if (string.Compare(reader.Name, ForeignKeyNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.name = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.tableName = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyRelatedTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            this.relatedTableName = reader.Value;
                                    }
                                }
                                if (emptyElement)
                                    return;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = false;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyRelatedFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = true;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, Field.FieldTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                Field f = Field.CreateFromXml(reader);
                                if (related)
                                    this.relatedFields.Add(i, f);
                                else
                                    this.fields.Add(i, f);
                                i++;
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return;
                        } break;
                    default: continue;
                }
            } while (reader.Read());
        }

        public static ForeignKey CreateFromXml(XmlReader reader)
        {
            //string uniqueName = String.Empty;
            string name = String.Empty;
            string tableName = String.Empty;
            string relatedTableName = String.Empty;
            Dictionary<int, IField> fields = new Dictionary<int, IField>();
            Dictionary<int, IField> relatedFields = new Dictionary<int, IField>();

            bool related = false;
            int i = 0;

            do
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        {
                            if (string.Compare(reader.Name, ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                bool emptyElement = reader.IsEmptyElement;
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        //if (string.Compare(reader.Name, ForeignKeyUniqueNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                        //    uniqueName = reader.Value;
                                        if (string.Compare(reader.Name, ForeignKeyNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            name = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            tableName = reader.Value;
                                        else if (string.Compare(reader.Name, ForeignKeyRelatedTableNameAttr, StringComparison.OrdinalIgnoreCase) == 0)
                                            relatedTableName = reader.Value;
                                    }
                                }
                                if (emptyElement)
                                    return new ForeignKey(tableName, fields, relatedTableName, relatedFields, name);
                            }
                            else if (string.Compare(reader.Name, ForeignKeyFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = false;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, ForeignKeyRelatedFieldsTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                related = true;
                                i = 0;
                            }
                            else if (string.Compare(reader.Name, Field.FieldTag, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                Field f = Field.CreateFromXml(reader);
                                if (related)
                                    relatedFields.Add(i, f);
                                else
                                    fields.Add(i, f);
                                i++;
                            }
                        } break;
                    case XmlNodeType.EndElement:
                        {
                            if (string.Compare(reader.Name, ForeignKeyTag, StringComparison.OrdinalIgnoreCase) == 0)
                                return new ForeignKey(tableName, fields, relatedTableName, relatedFields, name);
                        } break;
                    default: continue;
                }
            } while (reader.Read());
            return new ForeignKey(tableName, fields, relatedTableName, relatedFields, name);
        }

        public void WriteXml(XmlWriter writer)
        {
            //writer.WriteAttributeString(ForeignKeyUniqueNameAttr, this.UniqueName);
            writer.WriteAttributeString(ForeignKeyNameAttr, this.Name);
            writer.WriteAttributeString(ForeignKeyTableNameAttr, this.TableName);
            writer.WriteAttributeString(ForeignKeyRelatedTableNameAttr, this.RelatedTableName);
            if (this.Fields.Count() > 0)
            {
                writer.WriteStartElement(ForeignKeyFieldsTag);
                foreach (KeyValuePair<int, IField> f in this.Fields.OrderBy<KeyValuePair<int, IField>, int>(kv => kv.Key))
                {
                    writer.WriteStartElement(Field.FieldTag);
                    ((Field)f.Value).WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
            if (this.RelatedFields.Count() > 0)
            {
                writer.WriteStartElement(ForeignKeyRelatedFieldsTag);
                foreach (KeyValuePair<int, IField> f in this.RelatedFields.OrderBy<KeyValuePair<int, IField>, int>(kv => kv.Key))
                {
                    writer.WriteStartElement(Field.FieldTag);
                    ((Field)f.Value).WriteXml(writer);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
        }
        #endregion    

    }

}
